/**
 * Copyright 2010 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package saig.engine.graphics;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import saig.engine.Engine;
import saig.engine.Level;
import saig.engine.Settings;
import saig.engine.controllers.Controller;
import saig.engine.models.Tile;
import saig.engine.resources.TileLoader;
import saig.engine.utils.LevelUtil;


/**
 * Visual Engine Runner, this runner will run the game engine in visual mode so you can see the game play, or
 * participate in the game if using the HumanController.
 * @author Adam
 * @version $Id$
 * 
 * Created on May 27, 2010 at 7:06:07 PM 
 */
public class VisualRunner extends JPanel implements Runnable {
	/**
	 * Logger.
	 */
	private final Logger logger = LoggerFactory.getLogger(VisualRunner.class);
	/**
	 * Serial Version UID.
	 */
	private static final long serialVersionUID = -3318426329158477662L;
	/**
	 * The main window container for the visual runner.
	 */
	private JFrame container;
	/**
	 * The maximum number of frames that can be skipped in any animation loop.
	 */
	private static final int MAX_FRAME_SKIPS = 5;
	/**
	 * Number of frame with a delay of 0 ms before the animation thread yields
	 * to other running threads.
	 */
	private static final int NO_DELAYS_PER_YIELD = 16;
	/**
	 * The animation thread.
	 */
	private Thread animator;
	/**
	 * The background color to use.
	 */
	private Color bgColor = new Color(0x0065BD);
	/**
	 * Used for game termination.
	 */
	private volatile boolean gameOver = false;
	/**
	 * Graphics buffer used for off-screen rendering.
	 */
    private Graphics gBuffer;
    /**
     * Image buffer used for off-screen rendering.
     */
	private Image iBuffer = null;  //  @jve:decl-index=0:
	/**
	 * True if the game is paused.
	 */
	private volatile boolean paused = false;
	/**
	 * Period between drawing in nano seconds.
	 */
	private long period;
	/**
	 * True if the game is running.
	 */
    private volatile boolean running = false;
    /**
     * The Game Engine, carries out all the game logic.
     */
    private Engine gameEngine;
    /**
     * Enables the FPS debugging.
     */
    private boolean enableFps;
    /**
     * The game controller.
     */
    private final Controller controller;
    /**
     * Used to track the number of Frames per second if the enableFps is set to true.
     */
    private int frame = 0;
    /**
     * Used to track the number of ticks per second if enableFps is set to true.
     */
    private int ticks = 0;
    /**
     * Constructs a Visual Runner with the game period and a game controller.
     * @param fps The frames per second.
     * @param controller
     */
	public VisualRunner(final Settings settings) {
		 // Calculates the period based on the frames per second.
		this.period = ((long) (1000.0 / settings.getFps())) * 1000000L;
		this.controller = settings.getController(); // Sets the game controller.
		Level level = LevelUtil.loadLevel(getClass().getResourceAsStream("/level.xml"));
		gameEngine = new Engine(level, settings.getClock());
		setDoubleBuffered(false);
		setBackground(bgColor);
		setPreferredSize(new Dimension(Settings.SCREEN_WIDTH, Settings.SCREEN_HEIGHT));
		container = new JFrame("Game Visual Runner");
		container.add(this);
		container.addWindowListener(new WindowAdapter() {
			public void windowActivated(WindowEvent e) {
				resumeGame();
			}
			public void windowClosing(WindowEvent e) {
				stopGame();
			}
			public void windowDeactivated(WindowEvent e) {
				pauseGame();
			}
			public void windowDeiconified(WindowEvent e) {
				resumeGame();
			}
			public void windowIconified(WindowEvent e) {
				pauseGame();
			}
		});
		// If the controller takes keyboard input add it.
		if (this.controller instanceof KeyListener) {
			container.addKeyListener((KeyListener) this.controller);
		}
		// This will be used for pausing and ending the game.
		container.addKeyListener(new KeyAdapter() {
			public void keyPressed(final KeyEvent e) {
				keyEventHandler(e);
			}
		});
		
		// Repaint the level pane for animation.
		ActionListener repainter = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				for (Tile tile : TileLoader.getInstance().getTiles()) {
					tile.advanceFrame();
				}
			}
		};
		new Timer(150, repainter).start();
		if (enableFps) {
			ActionListener fpstimer = new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					logger.debug("Frames [{}], Ticks [{}]", frame, ticks);
					frame = 0;
					ticks = 0;
				}
			};
			new Timer(1000, fpstimer).start();
		}
		container.setLocationRelativeTo(null);
		container.setResizable(false);
		container.setFocusable(true);
		
		container.pack();
		container.setVisible(true);
		container.requestFocus();
		logger.debug("Width {}, Height {}", container.getWidth(), container.getHeight());

	}

	/**
	 * {@inheritDoc}
	 * @see javax.swing.JComponent#addNotify()
	 */
	@Override
	public void addNotify() {
		super.addNotify();
		startGame();
	}

	/**
	 * Renders the game.
	 */
	private void gameRender() {
		if (iBuffer == null) {
			iBuffer = createImage(Settings.SCREEN_WIDTH, Settings.SCREEN_HEIGHT);
			if (iBuffer == null) {
				logger.error("Unable to create image buffer");
			} else {
				gBuffer = iBuffer.getGraphics();
			}
			
		}
		// Clear the back ground.
		gBuffer.setColor(bgColor);
		gBuffer.fillRect(0, 0, Settings.SCREEN_WIDTH, Settings.SCREEN_HEIGHT);
		
		// Render level.
		gameEngine.doRender(gBuffer, Settings.SCREEN_WIDTH, Settings.SCREEN_HEIGHT);
		if (enableFps) {
			frame++;
		}
	}

	/**
	 * Updates the game state.
	 */
	private void gameUpdate() {
		if (!paused && !gameOver) {
			final boolean[] input = controller.getInput(gameEngine.getEnvironment());
			gameEngine.performAction(input);
			gameEngine.tick();
			if (enableFps) {
				ticks++;
			}
			// TODO need to get game over status.
			gameOver = gameEngine.isGameOver();
		}
	}

	/**
	 * {@inheritDoc}
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 */
	@Override
	public void paintComponent(final Graphics graphics) {
		super.paintComponents(graphics);
		if (iBuffer != null) {
			graphics.drawImage(iBuffer, 0, 0, null);
		}
	}
	/**
	 * Draw.
	 */
	private void paintScreen() {
		Graphics g;
		try {
			g = this.getGraphics();
			if ((g != null) && iBuffer != null) {
				g.drawImage(iBuffer, 0, 0, null);
				Toolkit.getDefaultToolkit().sync(); // sync the display.
				g.dispose();
			}
		} catch (Exception e) {
			logger.error("Graphics context error, unable to paintScreen.", e);
		}
	}

	/**
	 * Pauses the game.
	 */
	public void pauseGame() {
		paused = true;
	}

	/**
	 * Keyboard Event Handler.
	 * @param e The key event.
	 */
	private void keyEventHandler(final KeyEvent e) {
		final int keyCode = e.getKeyCode();
		if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_Q || keyCode == KeyEvent.VK_END
				|| (keyCode == KeyEvent.VK_C && e.isControlDown())) {
			running = false;
		}
		if (keyCode == KeyEvent.VK_PAUSE || keyCode == KeyEvent.VK_P) {
			paused = !paused;
		}
	}
	
	/**
	 * The main game loop for the Visual Runner.
	 * {@inheritDoc}
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		long beforeTime, afterTime, timeDiff, sleepTime;
		long overSleepTime = 0L;
		int noDelays = 0;
		long excess = 0L;
		
		beforeTime = System.nanoTime();
		running = true;
		while (running) {
			gameUpdate(); // state is updated.
			gameRender(); // render to buffer
			paintScreen(); // draw buffer to screen.
			
			afterTime = System.nanoTime();
			timeDiff = afterTime - beforeTime;
			sleepTime = (period - timeDiff) - overSleepTime;
			
			if (sleepTime > 0) {
				try {
					Thread.sleep(sleepTime / 1000000L); // nano --> ms
				} catch(final InterruptedException ex) {
					logger.error("Error putting thread to sleep.", ex);
				}
				overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
			} else {
				// sleepTime <= 0; frame took longer than period.
				excess -= sleepTime;
				overSleepTime = 0L;
				if (++noDelays >= NO_DELAYS_PER_YIELD) {
					Thread.yield(); // give another thread a chance to run.
					noDelays = 0;
				}
			}
			
			beforeTime = System.nanoTime();
			int skips = 0;
			while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
				excess -= period;
				gameUpdate(); // Updates state without rendering.
				skips++;
			}
		}
		System.exit(0);
	}

	/**
	 * Resumes the game from a pause.
	 */
	public void resumeGame() {
		paused = false;
	}

	/**
	 * Starts the game.
	 */
	public void startGame() {
		if (animator == null || ! running) {
			animator = new Thread(this);
			animator.start();
		}
	}

	/**
	 * Stops the game.
	 */
	public void stopGame() {
		running = false;
	}

	/**
	 * Returns enableFps.
	 * @return the enableFps.
	 */
	public boolean isEnableFps() {
		return enableFps;
	}

	/**
	 * Sets enableFps.
	 * @param enableFps the enableFps to set.
	 */
	public void setEnableFps(boolean enableFps) {
		this.enableFps = enableFps;
	}
}
